--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit ea7ef09660250fa10825adaa09995f6c6c763d6d
Parents : 8048cae
Author : Sudo-Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-01-01T20:00:59-06:00
feat(call): add remote icon retrieval for active calls and voicemails, implement send-to-voicemail functionality, and enhance call history with user icons
Changes
Diff
diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py
index e6b83768..516762e6 100644
--- a/meshchatx/meshchat.py
+++ b/meshchatx/meshchat.py
@@ -3010,6 +3010,7 @@ class ReticulumMeshChat:
# get remote identity hash
remote_identity_hash = None
remote_identity_name = None
+ remote_icon = None
remote_identity = telephone_active_call.get_remote_identity()
if remote_identity is not None:
remote_identity_hash = remote_identity.hash.hex()
@@ -3017,6 +3018,14 @@ class ReticulumMeshChat:
remote_identity_hash,
)
+ # get lxmf destination hash to look up icon
+ lxmf_destination_hash = RNS.Destination.hash(
+ remote_identity,
+ "lxmf",
+ "delivery",
+ ).hex()
+ remote_icon = self.database.misc.get_user_icon(lxmf_destination_hash)
+
active_call = {
"hash": telephone_active_call.hash.hex(),
"status": self.telephone_manager.telephone.call_status,
@@ -3024,6 +3033,7 @@ class ReticulumMeshChat:
"is_outgoing": telephone_active_call.is_outgoing,
"remote_identity_hash": remote_identity_hash,
"remote_identity_name": remote_identity_name,
+ "remote_icon": dict(remote_icon) if remote_icon else None,
"audio_profile_id": self.telephone_manager.telephone.transmit_codec.profile
if hasattr(
self.telephone_manager.telephone.transmit_codec,
@@ -3086,6 +3096,29 @@ class ReticulumMeshChat:
},
)
+ # send active call to voicemail
+ @routes.get("/api/v1/telephone/send-to-voicemail")
+ async def telephone_send_to_voicemail(request):
+ active_call = self.telephone_manager.telephone.active_call
+ if not active_call:
+ return web.json_response({"message": "No active call"}, status=404)
+
+ caller_identity = active_call.get_remote_identity()
+ if not caller_identity:
+ return web.json_response({"message": "No remote identity"}, status=400)
+
+ # trigger voicemail session
+ await asyncio.to_thread(
+ self.voicemail_manager.start_voicemail_session,
+ caller_identity,
+ )
+
+ return web.json_response(
+ {
+ "message": "Call sent to voicemail",
+ },
+ )
+
# mute/unmute transmit
@routes.get("/api/v1/telephone/mute-transmit")
async def telephone_mute_transmit(request):
@@ -3113,9 +3146,22 @@ class ReticulumMeshChat:
async def telephone_history(request):
limit = int(request.query.get("limit", 10))
history = self.database.telephone.get_call_history(limit=limit)
+
+ call_history = []
+ for row in history:
+ d = dict(row)
+ remote_identity_hash = d.get("remote_identity_hash")
+ if remote_identity_hash:
+ lxmf_hash = self.get_lxmf_destination_hash_for_identity_hash(remote_identity_hash)
+ if lxmf_hash:
+ icon = self.database.misc.get_user_icon(lxmf_hash)
+ if icon:
+ d["remote_icon"] = dict(icon)
+ call_history.append(d)
+
return web.json_response(
{
- "call_history": [dict(row) for row in history],
+ "call_history": call_history,
},
)
@@ -3267,13 +3313,26 @@ class ReticulumMeshChat:
async def telephone_voicemails(request):
limit = int(request.query.get("limit", 50))
offset = int(request.query.get("offset", 0))
- voicemails = self.database.voicemails.get_voicemails(
+ voicemails_rows = self.database.voicemails.get_voicemails(
limit=limit,
offset=offset,
)
+
+ voicemails = []
+ for row in voicemails_rows:
+ d = dict(row)
+ remote_identity_hash = d.get("remote_identity_hash")
+ if remote_identity_hash:
+ lxmf_hash = self.get_lxmf_destination_hash_for_identity_hash(remote_identity_hash)
+ if lxmf_hash:
+ icon = self.database.misc.get_user_icon(lxmf_hash)
+ if icon:
+ d["remote_icon"] = dict(icon)
+ voicemails.append(d)
+
return web.json_response(
{
- "voicemails": [dict(row) for row in voicemails],
+ "voicemails": voicemails,
"unread_count": self.database.voicemails.get_unread_count(),
},
)
@@ -6433,6 +6492,22 @@ class ReticulumMeshChat:
return None
# recall identity from reticulum or database
+ def get_lxmf_destination_hash_for_identity_hash(self, identity_hash: str):
+ identity = self.recall_identity(identity_hash)
+ if identity is not None:
+ return RNS.Destination.hash(identity, "lxmf", "delivery").hex()
+
+ # fallback to announces
+ announces = self.database.announces.get_filtered_announces(
+ aspect="lxmf.delivery",
+ search_term=identity_hash,
+ )
+ if announces:
+ for announce in announces:
+ if announce["identity_hash"] == identity_hash:
+ return announce["destination_hash"]
+ return None
+
def recall_identity(self, hash_hex: str) -> RNS.Identity | None:
try:
# 1. try reticulum recall (works for both identity and destination hashes)
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────